/**
 * @file exec.c
 * @date 23.05.01
 * @author xin
 * @brief 系统调用sys_exec会调用的exec()函数，以及其他辅助函数
 * @version 0.1
 * @status Stable
*/

#include "types.h"
#include "defs.h"
#include "elf.h"
#include "param.h"
#include "proc.h"

/**
 * @brief 从ip中向pagetable的虚拟地址va处加载一段程序段
 * @brief 其中va必须是页对齐的
 * @param pagetable 用于存放的页表
 * @param va 用于存放程序段的虚拟地址va
 * @param ip 获取程序段的文件系统节点
 * @param offset 文件系统节点的偏移量
 * @param sz 需要加载的程序段大小
 * @return 0:成功   -1:失败
*/
static int loadseg(pde_t* pagetable, uint64 va, struct inode* ip, uint offset, uint sz);

/**
 * @brief 获取页表的权限位并根据已有权限增添可执行/可写权限
 * @param flags 程序的flags字段
 * @return 新的权限位
*/
int
flags2perm(int flags)
{
    int perm = 0;
    if (flags & 0x1)    // 该PTE是否有效
        perm = PTE_X;
    if (flags & 0x2)    // 该PTE是否可读
        perm |= PTE_W;
    return perm;
}

int
exec(char *path, char **argv)
{
    char *s, *last;
    // 索引，elf头地址开始的偏移量
    int i, off;     
    uint64 argc, sz = 0, sp, ustack[MAXARG], stackbase;
    struct elfhdr elf;
    struct inode *ip;
    struct proghdr ph;
    pagetable_t pagetable = 0, old_pagetable;
    struct proc *p = myproc();
    uint64 sz1;

    begin_op();

    // 从path获取相应文件节点
    if ((ip = namei(path)) == 0) {
        return -1;
    }

    ilock(ip);

    // 检查ELF头，赋值到elf中
    if (readi(ip, 0, (uint64)&elf, 0, sizeof(elf)) != sizeof(elf))
        goto bad;

    // 检查elf魔数
    if (elf.magic != ELF_MAGIC)
        goto bad;

    // 为程序段申请页表
    if ((pagetable = proc_pagetable(p)) == 0)
        goto bad;
    
    // 加载程序到内存
    for (i = 0, off = elf.phoff; i < elf.phnum; i++, off += sizeof(ph)) {
        // 检查elf中位于off偏移的程序头，赋值到ph中取
        if (readi(ip, 0, (uint64)&ph, off, sizeof(ph)) != sizeof(ph))
            goto bad;
        // 检查是否为ELF程序头（若为ELF程序头，表示所描述的段要从可执行文件加载到内存中去）
        if (ph.type != ELF_PROG_LOAD)
            continue;
        // 检查内存大小与文件大小差异
        if (ph.memsz < ph.filesz)
            goto bad;
        // 检查内存大小非负
        if (ph.vaddr + ph.memsz < ph.vaddr)
            goto bad;
        // 虚拟地址页面对齐
        if (ph.vaddr % PGSIZE != 0)
            goto bad;
        // 虚拟内存从sz大小扩容到(ph.vaddr+ph.memsz)大小
        // 以容下filesz大小的容量
        // 并给新扩容的空间给予相应的权限
        if ((sz1 = uvmalloc(pagetable, sz, ph.vaddr+ph.memsz, flags2perm(ph.flags))) == 0)
            goto bad;
        sz = sz1;
        // 加载程序到页表
        if (loadseg(pagetable, ph.vaddr, ip, ph.off, ph.filesz) < 0)
            goto bad;
    }
    iunlockput(ip);
    end_op();
    ip = 0;

    p = myproc();
    uint64 oldsz = p->sz;

    // 在下一页边界申请两页
    sz = PGROUNDUP(sz);
    if ((sz1 = uvmalloc(pagetable, sz, sz + 2*PGSIZE, PTE_W)) == 0)
        goto bad;
    sz = sz1;
    // 第一页设置为无效页作为栈保护页
    uvmclear(pagetable, sz - 2*PGSIZE);
    sp = sz;
    // 第二页用于用户堆栈
    stackbase = sp - PGSIZE;

    // 获取参数，以string记录放置于用户栈中,并将每个参数的栈地址存放于ustack中
    for (argc = 0; argv[argc]; argc++) {
        if (argc >= MAXARG)
            goto bad;
        sp -= strlen(argv[argc]) + 1;
        sp -= sp % 16;      // 保证sp是16字节对齐的
        if (sp < stackbase) // 保证没有指向到无效保护页
            goto bad;
        if (copyout(pagetable, sp, argv[argc], strlen(argv[argc]) + 1) < 0)
            goto bad;
        ustack[argc] = sp;
    }
    ustack[argc] = 0;

    // 将存放参数的栈指针入栈
    sp -= (argc+1) * sizeof(uint64);
    sp -= sp % 16;      // 保证sp是16字节对齐的
    if (sp < stackbase) // 保证没有指向到无效保护页
        goto bad;
    if (copyout(pagetable, sp, (char*)ustack, (argc+1)*sizeof(uint64)) < 0)
        goto bad;

    // trapframe里的a1存放sp
    p->trapframe->a1 = sp;

    //为了debug，先保存程序的名称
    for (last = s = path; *s; s++)
        if (*s == '/')
            last = s+1;
    safestrcpy(p->name, last, sizeof(p->name));

    // 提交到用户映像
    old_pagetable = p->pagetable;
    p->pagetable = pagetable;
    p->sz = sz;
    p->trapframe->epc = elf.entry;      // 此时的返回地址变为elf的程序入口：main函数
    p->trapframe->sp = sp;              // 赋值进程的栈指针为指向参数的栈指针
    // 释放进程原来的页表
    proc_freepagetable(old_pagetable, oldsz);

    return argc;    // 该变量存在a0中，对应main(argc, argv)中的第一个参数

bad:
    if (pagetable)
        proc_freepagetable(pagetable, sz);
    if (ip) {
        iunlockput(ip);
    }
    return -1;
}

static int
loadseg(pagetable_t pagetable, uint64 va, struct inode *ip, uint offset, uint sz)
{
    uint i, n;
    uint64 pa;

    for (i = 0; i < sz; i += PGSIZE) {
        pa = walkaddr(pagetable, va+i);
        if (pa == 0)
            panic("loadseg: address doesn't exist");
        if (sz - i < PGSIZE)    // 最后一页的残余空间
            n = sz - i;
        else
            n = PGSIZE;
        if (readi(ip, 0, (uint64)pa, offset+i, n) != n)
            return -1;
    }

    return 0;
}